package org.geotools.filter.text.ecql;

import java.io.StringReader;
import java.util.List;

import org.geotools.filter.IllegalFilterException;
import org.geotools.filter.text.commons.IToken;
import org.geotools.filter.text.commons.Result;
import org.geotools.filter.text.commons.TokenAdapter;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.generated.parsers.ECQLParser;
import org.geotools.filter.text.generated.parsers.Node;
import org.geotools.filter.text.generated.parsers.ParseException;
import org.geotools.filter.text.generated.parsers.TokenMgrError;
import org.opengis.filter.BinaryComparisonOperator;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.Not;
import org.opengis.filter.Or;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.spatial.BinarySpatialOperator;
import org.opengis.filter.spatial.DistanceBufferOperator;
import org.opengis.filter.temporal.After;
import org.opengis.filter.temporal.Before;
import org.opengis.filter.temporal.During;

public class MqECQLCompiler extends ECQLParser implements
		org.geotools.filter.text.commons.ICompiler {

	private static final String ATTRIBUTE_PATH_SEPARATOR = ".";

	/** cql expression to compile */
	private final String source;

	private final ECQLFilterBuilder builder;

	/**
	 * new instance of TXTCompiler
	 * 
	 * @param txtSource
	 * @param filterFactory
	 */
	public MqECQLCompiler(final String txtSource,
			final FilterFactory filterFactory) {

		super(new StringReader(txtSource));

		assert txtSource != null : "txtSource cannot be null";
		assert filterFactory != null : "filterFactory cannot be null";

		this.source = txtSource;
		this.builder = new ECQLFilterBuilder(txtSource, filterFactory);
	}

	/**
	 * compile source to produce a Filter. The filter result must be retrieved
	 * with {@link #getFilter()}.
	 * 
	 * @throws CQLException
	 */
	public void compileFilter() throws CQLException {
		try {
			super.FilterCompilationUnit();
		} catch (TokenMgrError tokenError) {
			throw new CQLException(tokenError.getMessage(),
					getTokenInPosition(0), this.source);
		} catch (CQLException e) {
			throw e;

		} catch (ParseException e) {
			throw new CQLException(e.getMessage(), getTokenInPosition(0),
					e.getCause(), this.source);
		}
	}

	/**
	 * compiles source to produce a Expression
	 */
	public void compileExpression() throws CQLException {
		try {
			super.ExpressionCompilationUnit();
		} catch (TokenMgrError tokenError) {
			throw new CQLException(tokenError.getMessage(),
					getTokenInPosition(0), this.source);
		} catch (CQLException e) {
			throw e;
		} catch (ParseException e) {
			throw new CQLException(e.getMessage(), getTokenInPosition(0),
					e.getCause(), this.source);
		}
	}

	/**
	 * Compiles a list of filters
	 */
	public void compileFilterList() throws CQLException {
		try {
			super.FilterListCompilationUnit();
		} catch (TokenMgrError tokenError) {
			throw new CQLException(tokenError.getMessage(),
					getTokenInPosition(0), this.source);
		} catch (CQLException e) {
			throw e;
		} catch (ParseException e) {
			throw new CQLException(e.getMessage(), getTokenInPosition(0),
					e.getCause(), this.source);
		}
	}

	/**
	 * @return the ECQLsource
	 */
	public final String getSource() {
		return this.source;
	}

	/**
	 * Return the filter resultant of compiling process
	 * 
	 * @return Filter
	 * @throws CQLException
	 */
	public final Filter getFilter() throws CQLException {
		return this.builder.getFilter();
	}

	/**
	 * Return the expression resultant of compiling process
	 * 
	 * @return Expression
	 * @throws CQLException
	 */
	public final Expression getExpression() throws CQLException {

		return this.builder.getExpression();
	}

	public IToken getTokenInPosition(int index) {
		return TokenAdapter.newAdapterFor(super.getToken(index));
	}

	/**
	 * Returns the list of Filters built as the result of calling
	 * {@link #FilterListCompilationUnit()()}
	 * 
	 * @return List<Filter>
	 * @throws CQLException
	 *             if a ClassCastException occurs while casting a built item to
	 *             a Filter.
	 */
	public List<Filter> getFilterList() throws CQLException {
		return this.builder.getFilterList();
	}

	public final void jjtreeOpenNodeScope(Node n) {
	}

	/**
	 * called by parser when the node is closed.
	 */
	public final void jjtreeCloseNodeScope(Node n) throws ParseException {

		try {
			Object built = build(n);

			IToken tokenAdapter = TokenAdapter.newAdapterFor(this.token);
			Result r = new Result(built, tokenAdapter, n.getType());
			this.builder.pushResult(r);

		} catch (CQLException e) {
			throw e;

		} finally {
			n.dispose();
		}
	}

	/**
	 * This method is called when the parser close a node. Here is built the
	 * filters an expressions recognized in the parsing process.
	 * 
	 * @param n
	 *            a Node instance
	 * @return Filter or Expression
	 * @throws CQLException
	 */
	private Object build(Node n) throws CQLException {

		switch (n.getType()) {

		// ----------------------------------------
		// (+|-) Integer and Float
		// ----------------------------------------
		case JJTINTEGERNODE:
			return this.builder.buildLiteralInteger(getTokenInPosition(0)
					.toString());
		case JJTFLOATINGNODE:
			return this.builder.buildLiteralDouble(getTokenInPosition(0)
					.toString());
		case JJTNEGATIVENUMBER_NODE:
			return this.builder.bulidNegativeNumber();

			// ----------------------------------------
			// String
			// ----------------------------------------
		case JJTSTRINGNODE:
			return this.builder.buildLiteralString(getTokenInPosition(0)
					.toString());

			// ----------------------------------------
			// Identifier
			// ----------------------------------------
		case JJTIDENTIFIER_NODE:
			return this.builder.buildIdentifier(JJTIDENTIFIER_PART_NODE);

		case JJTIDENTIFIER_PART_NODE:
			return this.builder.buildIdentifierPart(getTokenInPosition(0));

			// ----------------------------------------
			// attribute
			// ----------------------------------------
		case JJTSIMPLE_ATTRIBUTE_NODE:
			return this.builder.buildSimpleAttribute();

		case JJTCOMPOUND_ATTRIBUTE_NODE:
			return this.builder.buildCompoundAttribute(
					JJTSIMPLE_ATTRIBUTE_NODE, ATTRIBUTE_PATH_SEPARATOR);

			// ----------------------------------------
			// function
			// ----------------------------------------
		case JJTFUNCTION_NODE:
			return this.builder.buildFunction(JJTFUNCTIONNAME_NODE);

		case JJTFUNCTIONNAME_NODE:
			return n; // used as mark of function name in stack

		case JJTFUNCTIONARG_NODE:
			return n; // used as mark of args in stack

			// Math Nodes
		case JJTADDNODE:
		case JJTSUBTRACTNODE:
		case JJTMULNODE:
		case JJTDIVNODE:
			return buildBinaryExpression(n.getType());

			// Boolean expression
		case JJTBOOLEAN_AND_NODE:
			return buildLogicFilter(JJTBOOLEAN_AND_NODE);

		case JJTBOOLEAN_OR_NODE:
			return buildLogicFilter(JJTBOOLEAN_OR_NODE);

		case JJTBOOLEAN_NOT_NODE:
			return buildLogicFilter(JJTBOOLEAN_NOT_NODE);

			// ----------------------------------------
			// between predicate actions
			// ----------------------------------------
		case JJTBETWEEN_NODE:
			return this.builder.buildBetween();

		case JJTNOT_BETWEEN_NODE:
			return this.builder.buildNotBetween();

			// ----------------------------------------
			// Compare predicate actions
			// ----------------------------------------
		case JJTCOMPARISONPREDICATE_EQ_NODE:
		case JJTCOMPARISONPREDICATE_GT_NODE:
		case JJTCOMPARISONPREDICATE_LT_NODE:
		case JJTCOMPARISONPREDICATE_GTE_NODE:
		case JJTCOMPARISONPREDICATE_LTE_NODE:
			return buildBinaryComparasionOperator(n.getType());

		case JJTCOMPARISONPREDICATE_NOT_EQUAL_NODE:

			Filter eq = buildBinaryComparasionOperator(JJTCOMPARISONPREDICATE_EQ_NODE);
			Not notFilter = this.builder.buildNotFilter(eq);

			return notFilter;

			// ----------------------------------------
			// Text predicate (Like)
			// ----------------------------------------
		case JJTLIKE_NODE:
			return this.builder.buildLikeFilter(true);

		case JJTNOT_LIKE_NODE:

			return this.builder.buildNotLikeFilter(true);

			// ----------------------------------------
			// Text predicate (ILike)
			// ----------------------------------------
		case JJTILIKE_NODE:
			return this.builder.buildLikeFilter(false);

		case JJTNOT_ILIKE_NODE:

			return this.builder.buildNotLikeFilter(false);

			// ----------------------------------------
			// Null predicate
			// ----------------------------------------
		case JJTNULLPREDICATENODE:
			return this.builder.buildPropertyIsNull();

		case JJTNOTNULLPREDICATENODE:
			return this.builder.buildPorpertyNotIsNull();

			// ----------------------------------------
			// temporal predicate actions
			// ----------------------------------------
		case JJTDATE_NODE:
			return this.builder.buildDateExpression(getTokenInPosition(0));

		case JJTDATETIME_NODE:
			return this.builder.buildDateTimeExpression(getTokenInPosition(0));

		case JJTDURATION_DATE_NODE:
			return this.builder.buildDurationExpression(getTokenInPosition(0));

		case JJTPERIOD_BETWEEN_DATES_NODE:
			return this.builder.buildPeriodBetweenDates();

		case JJTPERIOD_WITH_DATE_DURATION_NODE:
			return this.builder.buildPeriodDateAndDuration();

		case JJTPERIOD_WITH_DURATION_DATE_NODE:
			return this.builder.buildPeriodDurationAndDate();

		case JJTTPTEQUALS_DATETIME_NODE:
			return this.builder.buildTEquals();

		case JJTTPBEFORE_DATETIME_NODE:
			return buildBefore();

		case JJTTPAFTER_DATETIME_NODE:
			return buildAfterPredicate();

		case JJTTPDURING_PERIOD_NODE:
			return buildDuring();

		case JJTTPBEFORE_OR_DURING_PERIOD_NODE:
			return buildBeforeOrDuring();

		case JJTTPDURING_OR_AFTER_PERIOD_NODE:
			return buildDuringOrAfter();

			// ----------------------------------------
			// existence predicate actions
			// ----------------------------------------
		case JJTEXISTENCE_PREDICATE_EXISTS_NODE:
			return this.builder.buildPropertyExists();

		case JJTEXISTENCE_PREDICATE_DOESNOTEXIST_NODE:

			Filter filter = this.builder.buildPropertyExists();
			Filter filterPropNotExist = this.builder.buildNotFilter(filter);

			return filterPropNotExist;

			// ----------------------------------------
			// routine invocation Geo Operation
			// ----------------------------------------
		case JJTROUTINEINVOCATION_GEOOP_EQUAL_NODE:
		case JJTROUTINEINVOCATION_GEOOP_DISJOINT_NODE:
		case JJTROUTINEINVOCATION_GEOOP_INTERSECT_NODE:
		case JJTROUTINEINVOCATION_GEOOP_TOUCH_NODE:
		case JJTROUTINEINVOCATION_GEOOP_CROSS_NODE:
		case JJTROUTINEINVOCATION_GEOOP_WITHIN_NODE:
		case JJTROUTINEINVOCATION_GEOOP_CONTAIN_NODE:
		case JJTROUTINEINVOCATION_GEOOP_OVERLAP_NODE:
			return buildBinarySpatialOperator(n.getType());

		case JJTROUTINEINVOCATION_GEOOP_BBOX_NODE:
		case JJTROUTINEINVOCATION_GEOOP_BBOX_SRS_NODE:
			return buildBBox(n.getType());

		case JJTROUTINEINVOCATION_GEOOP_RELATE_NODE:
			return this.builder.buildSpatialRelateFilter();

		case JJTDE9IM_NODE:
			return this.builder.buildDE9IM(getToken(0).image);

			// ----------------------------------------
			// routine invocation RelGeo Operation
			// ----------------------------------------
		case JJTTOLERANCE_NODE:
			return this.builder.buildTolerance();

		case JJTDISTANCEUNITS_NODE:
			return this.builder.buildDistanceUnit(getTokenInPosition(0));

		case JJTROUTINEINVOCATION_RELOP_BEYOND_NODE:
		case JJTROUTINEINVOCATION_RELOP_DWITHIN_NODE:
			return buildDistanceBufferOperator(n.getType());

			// ----------------------------------------
			// Geometries:
			// ----------------------------------------
		case JJTPOINT_NODE:
			return this.builder.buildCoordinate();

		case JJTPOINTTEXT_NODE:
			return this.builder.buildPointText();

		case JJTLINESTRINGTEXT_NODE:
			return this.builder.buildLineString(JJTPOINT_NODE);

		case JJTPOLYGONTEXT_NODE:
			return this.builder.buildPolygon(JJTLINESTRINGTEXT_NODE);

		case JJTMULTIPOINTTEXT_NODE:
			return this.builder.buildMultiPoint(JJTPOINTTEXT_NODE);

		case JJTMULTILINESTRINGTEXT_NODE:
			return this.builder.buildMultiLineString(JJTLINESTRINGTEXT_NODE);

		case JJTMULTIPOLYGONTEXT_NODE:
			return this.builder.buildMultiPolygon(JJTPOLYGONTEXT_NODE);

		case JJTGEOMETRYLITERAL:
			return this.builder.buildGeometryLiteral();

		case JJTGEOMETRYCOLLECTIONTEXT_NODE:
			return this.builder.buildGeometryCollection(JJTGEOMETRYLITERAL);

		case JJTWKTNODE:
			return this.builder.buildGeometry();

		case JJTENVELOPETAGGEDTEXT_NODE:
			return this.builder.buildEnvelop(TokenAdapter.newAdapterFor(n
					.getToken()));

		case JJTINCLUDE_NODE:
			return Filter.INCLUDE;

		case JJTEXCLUDE_NODE:
			return Filter.EXCLUDE;

		case JJTTRUENODE:
			return this.builder.buildTrueLiteral();

		case JJTFALSENODE:
			return this.builder.buildFalseLiteral();

			// ----------------------------------------
			// ID Predicate
			// ----------------------------------------
		case JJTFEATURE_ID_NODE:
			return this.builder.buildFeatureID(getTokenInPosition(0));

		case JJTID_PREDICATE_NODE:
			return this.builder.buildFilterId(JJTFEATURE_ID_NODE);
			// ----------------------------------------
			// IN Predicate
			// ----------------------------------------
		case JJTIN_PREDICATE_NODE:
			return this.builder.buildInPredicate(JJTEXPRESSION_IN_LIST_NODE);

		case JJTNOT_IN_PREDICATE_NODE:
			Or orFilter = this.builder
					.buildInPredicate(JJTEXPRESSION_IN_LIST_NODE);
			Not notOrFilter = this.builder.buildNotFilter(orFilter);

			return notOrFilter;
		}

		return null;
	}

	private org.opengis.filter.expression.BinaryExpression buildBinaryExpression(
			int nodeType) throws CQLException {

		org.opengis.filter.expression.BinaryExpression expr = null;

		switch (nodeType) {
		case JJTADDNODE:
			expr = this.builder.buildAddExpression();

			break;

		case JJTSUBTRACTNODE:
			expr = this.builder.buildSubtractExression();

			break;

		case JJTMULNODE:

			expr = this.builder.buildMultiplyExpression();
			break;

		case JJTDIVNODE:

			expr = this.builder.buildDivideExpression();
			break;

		default:
			break;
		}

		return expr;
	}

	private Filter buildLogicFilter(int nodeType) throws CQLException {
		try {
			Filter logicFilter;

			switch (nodeType) {
			case JJTBOOLEAN_AND_NODE:
				logicFilter = this.builder.buildAndFilter();
				break;

			case JJTBOOLEAN_OR_NODE:
				logicFilter = this.builder.buildOrFilter();
				break;

			case JJTBOOLEAN_NOT_NODE:

				logicFilter = this.builder.buildNotFilter();
				break;

			default:
				throw new CQLException(
						"Expression not supported. And, Or, Not is required",
						getTokenInPosition(0), this.source);
			}

			return logicFilter;
		} catch (IllegalFilterException ife) {
			throw new CQLException("Exception building LogicFilter",
					getTokenInPosition(0), ife, this.source);
		}
	}

	/**
	 * Creates Binary Spatial Operator
	 * 
	 * @param tipeNode
	 * 
	 * @return BinarySpatialOperator
	 * @throws CQLException
	 */
	private BinarySpatialOperator buildBinarySpatialOperator(final int nodeType)
			throws CQLException {

		BinarySpatialOperator filter = null;

		switch (nodeType) {
		case JJTROUTINEINVOCATION_GEOOP_EQUAL_NODE:

			filter = this.builder.buildSpatialEqualFilter();
			break;
		case JJTROUTINEINVOCATION_GEOOP_DISJOINT_NODE:
			filter = this.builder.buildSpatialDisjointFilter();
			break;
		case JJTROUTINEINVOCATION_GEOOP_INTERSECT_NODE:
			filter = this.builder.buildSpatialIntersectsFilter();
			break;
		case JJTROUTINEINVOCATION_GEOOP_TOUCH_NODE:
			filter = this.builder.buildSpatialTouchesFilter();
			break;

		case JJTROUTINEINVOCATION_GEOOP_CROSS_NODE:
			filter = this.builder.buildSpatialCrossesFilter();
			break;
		case JJTROUTINEINVOCATION_GEOOP_WITHIN_NODE:
			filter = this.builder.buildSpatialWithinFilter();
			break;
		case JJTROUTINEINVOCATION_GEOOP_CONTAIN_NODE:
			filter = this.builder.buildSpatialContainsFilter();
			break;
		case JJTROUTINEINVOCATION_GEOOP_OVERLAP_NODE:
			filter = this.builder.buildSpatialOverlapsFilter();
			break;
		default:
			throw new CQLException("Binary spatial operator unexpected");
		}

		return filter;
	}

	private org.opengis.filter.spatial.BBOX buildBBox(int nodeType)
			throws CQLException {

		if (nodeType == JJTROUTINEINVOCATION_GEOOP_BBOX_SRS_NODE) {
			return this.builder.buildBBoxWithCRS();
		} else {
			return this.builder.buildBBox();
		}
	}

	/**
	 * Builds Distance Buffer Operator
	 * 
	 * @param nodeType
	 * @return DistanceBufferOperator dwithin and beyond filters
	 * @throws CQLException
	 */
	private DistanceBufferOperator buildDistanceBufferOperator(
			final int nodeType) throws CQLException {

		DistanceBufferOperator filter = null;

		switch (nodeType) {
		case JJTROUTINEINVOCATION_RELOP_DWITHIN_NODE:
			filter = this.builder.buildSpatialDWithinFilter();
			break;

		case JJTROUTINEINVOCATION_RELOP_BEYOND_NODE:

			filter = this.builder.buildSpatialBeyondFilter();
			break;

		default:
			throw new CQLException("Binary spatial operator unexpected");
		}

		return filter;
	}

	private org.opengis.filter.Filter buildBeforeOrDuring() throws CQLException {
		org.opengis.filter.Filter filter = null;

		Result node = this.builder.peekResult();

		switch (node.getNodeType()) {
		case JJTPERIOD_BETWEEN_DATES_NODE:
		case JJTPERIOD_WITH_DATE_DURATION_NODE:
		case JJTPERIOD_WITH_DURATION_DATE_NODE:
			filter = this.builder.buildBeforeOrDuring();
			break;

		default:
			throw new CQLException(
					"unexpeted date time expression in temporal predicate.",
					node.getToken(), this.source);
		}

		return filter;
	}

	private Or buildDuringOrAfter() throws CQLException {
		Or filter = null;

		Result node = this.builder.peekResult();

		switch (node.getNodeType()) {
		case JJTPERIOD_BETWEEN_DATES_NODE:
		case JJTPERIOD_WITH_DATE_DURATION_NODE:
		case JJTPERIOD_WITH_DURATION_DATE_NODE:
			filter = this.builder.buildDuringOrAfter();

			break;

		default:
			throw new CQLException(
					"unexpeted date time expression in temporal predicate.",
					node.getToken(), this.source);
		}

		return filter;
	}

	/**
	 * Build the convenient filter for before date and before period filters
	 * 
	 * @param nodeType
	 * 
	 * @return Filter
	 * @throws CQLException
	 */
	private Before buildBefore() throws CQLException {
		Before filter = null;

		// analyzes if the last build is period or date
		Result node = this.builder.peekResult();

		switch (node.getNodeType()) {
		case JJTDATETIME_NODE:
			filter = this.builder.buildBeforeDate();
			break;

		case JJTPERIOD_BETWEEN_DATES_NODE:
		case JJTPERIOD_WITH_DATE_DURATION_NODE:
		case JJTPERIOD_WITH_DURATION_DATE_NODE:
			filter = this.builder.buildBeforePeriod();
			break;

		default:
			throw new CQLException(
					"unexpeted date time expression in temporal predicate.",
					node.getToken(), this.source);
		}

		return filter;
	}

	/**
	 * Build the convenient filter for during period filters
	 * 
	 * @return Filter
	 * @throws CQLException
	 */
	private During buildDuring() throws CQLException {
		During filter = null;

		// determines if the node is period or date
		Result node = this.builder.peekResult();

		switch (node.getNodeType()) {
		case JJTPERIOD_BETWEEN_DATES_NODE:
		case JJTPERIOD_WITH_DATE_DURATION_NODE:
		case JJTPERIOD_WITH_DURATION_DATE_NODE:
			filter = this.builder.buildDuringPeriod();
			break;

		default:
			throw new CQLException(
					"unexpeted period expression in temporal predicate.",
					node.getToken(), this.source);
		}

		return filter;
	}

	/**
	 * build filter for after date and after period
	 * 
	 * @return a filter
	 * @throws CQLException
	 */
	private After buildAfterPredicate() throws CQLException {
		After filter = null;

		// determines if the node is period or date
		Result node = this.builder.peekResult();

		switch (node.getNodeType()) {
		case JJTDATETIME_NODE:
			filter = this.builder.buildAfterDate();
			break;

		case JJTPERIOD_BETWEEN_DATES_NODE:
		case JJTPERIOD_WITH_DURATION_DATE_NODE:
		case JJTPERIOD_WITH_DATE_DURATION_NODE:
			filter = this.builder.buildAfterPeriod();
			break;

		default:
			throw new CQLException(
					"unexpeted date time expression in temporal predicate.",
					node.getToken(), this.source);
		}

		return filter;
	}

	/**
	 * Builds a compare filter
	 * 
	 * @param filterTipa
	 * 
	 * @return BinaryComparisonOperator
	 * @throws CQLException
	 */
	private BinaryComparisonOperator buildBinaryComparasionOperator(
			int filterType) throws CQLException {

		switch (filterType) {
		case JJTCOMPARISONPREDICATE_EQ_NODE:
			return this.builder.buildEquals();

		case JJTCOMPARISONPREDICATE_GT_NODE:
			return this.builder.buildGreater();

		case JJTCOMPARISONPREDICATE_LT_NODE:
			return this.builder.buildLess();

		case JJTCOMPARISONPREDICATE_GTE_NODE:
			return this.builder.buildGreaterOrEqual();

		case JJTCOMPARISONPREDICATE_LTE_NODE:
			return this.builder.buildLessOrEqual();

		default:
			throw new CQLException("unexpeted filter type.");
		}
	}
	
	

}



